<?php

namespace Dreamcat\Class2Array\Impl;

use DreamCat\Array2Class\Annotation\ObjectAttribute;
use DreamCat\Array2Class\PropertyNameTransInterface;
use DreamCat\Array2Class\Utils\FeatureJudge;
use Dreamcat\Class2Array\JsonValueFixer;
use Dreamcat\Class2Array\Utils\EnumHelper;
use JsonSerializable;
use ReflectionClass;
use ReflectionEnum;
use ReflectionProperty;
use stdClass;

/**
 * json数据修正器
 * @author vijay
 */
class DefaultJsonValueFixer implements JsonValueFixer
{
    /** @var int 属性过滤器 */
    private const PROPERTY_FILTER = ReflectionProperty::IS_PUBLIC
    | ReflectionProperty::IS_PROTECTED
    | ReflectionProperty::IS_PRIVATE;

    /** @var ?PropertyNameTransInterface 自动名称转换接口 */
    private ?PropertyNameTransInterface $propertyNameTrans = null;

    /** @var bool 是否忽略未初始化的属性 */
    private bool $ignoreUninit = false;

    /**
     * @return bool 是否忽略未初始化的属性
     */
    public function isIgnoreUninit(): bool
    {
        return $this->ignoreUninit;
    }

    /**
     * @param bool $ignoreUninit 是否忽略未初始化的属性
     * @return $this
     */
    public function setIgnoreUninit(bool $ignoreUninit): static
    {
        $this->ignoreUninit = $ignoreUninit;
        return $this;
    }

    /**
     * @return PropertyNameTransInterface|null 自动名称转换接口
     */
    public function getPropertyNameTrans(): ?PropertyNameTransInterface
    {
        return $this->propertyNameTrans;
    }

    /**
     * @param PropertyNameTransInterface $propertyNameTrans 自动名称转换接口
     */
    public function setPropertyNameTrans(PropertyNameTransInterface $propertyNameTrans): void
    {
        $this->propertyNameTrans = $propertyNameTrans;
    }

    /**
     * 修正数据，得到可以json化的数据
     * @param mixed $originalValue 原始数据
     * @return mixed 可以json化的数据
     */
    public function fixValue(mixed $originalValue): mixed
    {
        switch (true) {
            case $originalValue === null:
            case is_scalar($originalValue):
            case $originalValue instanceof JsonSerializable:
            case is_resource($originalValue):
            case is_callable($originalValue):
                return $originalValue;
            case is_array($originalValue):
                $ary = [];
                foreach ($originalValue as $key => $item) {
                    $ary[$key] = $this->fixValue($item);
                }
                return $ary;
            case EnumHelper::isEnumValue($originalValue):
                return $this->formatEnum($originalValue);
            case is_object($originalValue):
                return $this->formatData($originalValue);
            default:
                // @codeCoverageIgnoreStart
                return null;
        }
        // @codeCoverageIgnoreEnd
    }

    /**
     * 格式化对象数据
     * @param object $object
     * @return stdClass|array 能被 json_encode() 原生地序列化的值
     */
    protected function formatData(object $object): stdClass|array
    {
        $refClass = new ReflectionClass($object);

        $result = [];
        $propertyNames = [];

        do {
            foreach ($refClass->getProperties(self::PROPERTY_FILTER) as $property) {
                if (isset($propertyNames[$property->name])) {
                    continue;
                }
                $propertyNames[$property->name] = 1;
                if ($this->propertyIsIgnore($property)) {
                    continue;
                }
                $key = $this->getPropertyName($property);
                if (isset($result[$key])) {
                    continue;
                }
                if ($this->isIgnoreUninit() && !$property->isInitialized($object)) {
                    continue;
                }
                $result[$key] = $this->fixValue($this->getPropertyValue($property, $object));
            }
            $refClass = $refClass->getParentClass();
        } while ($refClass);

        foreach (get_object_vars($object) as $key => $var) {
            if (!isset($propertyNames[$key])) {
                $result[$key] = $this->fixValue($var);
            }
        }

        if (!$result) {
            $result = new stdClass();
        }
        return $result;
    }

    /**
     * 格式化内置枚举
     * @param object $enum
     * @return string|int
     */
    protected function formatEnum(object $enum): string|int
    {
        /** @noinspection PhpUnhandledExceptionInspection */
        $ref = new ReflectionEnum($enum);
        if ($ref->isBacked()) {
            return $enum->value;
        } else {
            return $enum->name;
        }
    }

    /**
     * 获取属性的值
     * @param ReflectionProperty $property
     * @param object $object
     * @return mixed
     */
    protected function getPropertyValue(ReflectionProperty $property, object $object): mixed
    {
        $public = $property->isPublic();
        if (!$public && FeatureJudge::needSetAccessible()) {
            // @codeCoverageIgnoreStart
            /** @noinspection PhpExpressionResultUnusedInspection */
            $property->setAccessible(true);
            // @codeCoverageIgnoreEnd
        }
        if ($property->isInitialized($object)) {
            $value = $property->getValue($object);
        } else {
            $value = null;
        }
        if (!$public && FeatureJudge::needSetAccessible()) {
            // @codeCoverageIgnoreStart
            /** @noinspection PhpExpressionResultUnusedInspection */
            $property->setAccessible(false);
            // @codeCoverageIgnoreEnd
        }
        return $value;
    }

    /**
     * 获取属性名称
     * @param ReflectionProperty $property 属性反射
     * @return string 合适的名称，要考虑别名、自动转换等
     */
    protected function getPropertyName(ReflectionProperty $property): string
    {
        $name = $property->name;
        $oas = $property->getAttributes(ObjectAttribute::class);
        $hasAlias = false;
        if ($oas) {
            /** @var ObjectAttribute $oa */
            $oa = $oas[0]->newInstance();
            if (strlen($oa->getAlias()) > 0) {
                $name = $oa->getAlias();
                $hasAlias = true;
            }
        }
        if (!$hasAlias && $this->propertyNameTrans) {
            $name = $this->propertyNameTrans->convert($name);
        }
        return $name;
    }

    /**
     * 判断一个属性是否可以忽略
     * @param ReflectionProperty $property 属性反射
     * @return bool 是否可忽略
     */
    protected function propertyIsIgnore(ReflectionProperty $property): bool
    {
        $oas = $property->getAttributes(ObjectAttribute::class);
        if ($oas) {
            /** @var ObjectAttribute $oa */
            $oa = $oas[0]->newInstance();
            $ignore = $oa->isIgnore();
        } else {
            $ignore = false;
        }
        return $ignore;
    }
}

# end of file
